# tefs_mini.py
import logging
import re
import sqlite3
import uuid
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from pathlib import Path

import math
import pysrt
import whisper
from flask import (Flask, request, render_template, redirect, url_for,
                   session, send_from_directory, flash, jsonify, abort)
from moviepy import (VideoFileClip, CompositeVideoClip, ColorClip, TextClip,
                   concatenate_videoclips, AudioFileClip, concatenate_audioclips, AudioClip)
from webvtt import webvtt
from werkzeug.security import generate_password_hash, check_password_hash
import chardet

import subprocess
import shutil
# ------------------ 基本配置 ------------------
BASE_DIR = Path(__file__).resolve().parent
UPLOAD_DIR = BASE_DIR / "uploads"
OUT_DIR = BASE_DIR / "outputs"
DB_PATH = BASE_DIR / "tefs_mini.db"
UPLOAD_DIR.mkdir(exist_ok=True)
OUT_DIR.mkdir(exist_ok=True)

app = Flask(__name__)
app.secret_key = "dev-only-replace-me"
logging.basicConfig(level=logging.INFO)
SUPPORTED_AUDIO_EXT = {'mp3', 'wav', 'aac', 'flac', 'm4a'}
# 全局模型缓存
model_cache = {}
task_progress = {}
# 创建线程池用于异步任务
executor = ThreadPoolExecutor(max_workers=4)

# 支持的模型和语言
SUPPORTED_MODELS = ["tiny", "base", "small", "medium", "large-v2"]
SUPPORTED_LANGUAGES = {
    "auto": "自动检测",
    "zh": "中文",
    "en": "英语",
    "ja": "日语",
    "ko": "韩语",
    "fr": "法语",
    "de": "德语",
    "es": "西班牙语",
    "ru": "俄语",
    "pt": "葡萄牙语"
}

# 支持的字幕文件扩展名
ALLOWED_SUBTITLE_EXT = {'srt', 'vtt', 'txt', 'ass', 'ssa'}

# 语气词列表（用于去杂功能）
FILLER_WORDS = ["嗯", "啊", "呃", "呢", "这个", "那个", "呃", "哦", "唔", "嘛", "吧", "呀", "啦", "哈"]

task_progress = {}
# ------------------ 简易数据库 ------------------
def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    with get_db() as conn:
        # 创建用户表
        conn.execute("""CREATE TABLE IF NOT EXISTS users (
                            id          INTEGER PRIMARY KEY AUTOINCREMENT,
                            username    TEXT UNIQUE NOT NULL,
                            password    TEXT NOT NULL,
                            created_at  DATETIME DEFAULT CURRENT_TIMESTAMP
                        )""")

        # 创建转写任务表
        conn.execute("""CREATE TABLE IF NOT EXISTS tasks (
                            id          INTEGER PRIMARY KEY AUTOINCREMENT,
                            user_id     INTEGER NOT NULL,
                            original_name TEXT NOT NULL,
                            safe_name   TEXT NOT NULL,
                            model_name  TEXT NOT NULL,
                            language    TEXT NOT NULL,
                            status      TEXT NOT NULL DEFAULT 'processing',
                            created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
                            completed_at DATETIME,
                            txt_name    TEXT,
                            cleaned     BOOLEAN DEFAULT 0,
                            FOREIGN KEY (user_id) REFERENCES users(id)
                        )""")

        # 创建字幕任务表 - 修复这里，添加 video_name 列
        conn.execute("""CREATE TABLE IF NOT EXISTS subtitle_tasks (
                            id          INTEGER PRIMARY KEY AUTOINCREMENT,
                            user_id     INTEGER NOT NULL,
                            video_name  TEXT NOT NULL,  -- 确保这行存在
                            video_safe_name TEXT NOT NULL,
                            subtitle_name TEXT NOT NULL,
                            output_name TEXT,
                            font_size   INTEGER DEFAULT 24,
                            font_color  TEXT DEFAULT 'white',
                            bg_color    TEXT DEFAULT 'black',
                            bg_opacity  REAL DEFAULT 0.7,
                            position    TEXT DEFAULT 'bottom',
                            status      TEXT NOT NULL DEFAULT 'processing',
                            created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
                            completed_at DATETIME,
                            FOREIGN KEY (user_id) REFERENCES users(id)
                        )""")

        # 创建音频替换任务表
        conn.execute("""CREATE TABLE IF NOT EXISTS audio_replace_tasks (
                            id          INTEGER PRIMARY KEY AUTOINCREMENT,
                            user_id     INTEGER NOT NULL,
                            video_name  TEXT NOT NULL,  --确保这行存在
                            video_safe_name TEXT NOT NULL,
                            audio_name  TEXT NOT NULL,
                            audio_safe_name TEXT NOT NULL,
                            start_time  REAL DEFAULT 0.0,
                            end_time    REAL DEFAULT NULL,
                            output_name TEXT,
                            status      TEXT NOT NULL DEFAULT 'processing',
                            created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
                            completed_at DATETIME,
                            FOREIGN KEY (user_id) REFERENCES users(id)
                        )""")

        # 尝试删除可能存在的旧列（如果存在）
        try:
            conn.execute("ALTER TABLE subtitle_tasks DROP COLUMN video_path")
        except sqlite3.OperationalError:
            pass  # 列不存在，忽略错误

        try:
            conn.execute("ALTER TABLE subtitle_tasks DROP COLUMN subtitle_path")
        except sqlite3.OperationalError:
            pass  # 列不存在，忽略错误

        # 添加可能缺失的其他列
        try:
            conn.execute("ALTER TABLE tasks ADD COLUMN cleaned BOOLEAN DEFAULT 0")
        except sqlite3.OperationalError:
            pass

        try:
            conn.execute("ALTER TABLE subtitle_tasks ADD COLUMN bg_opacity REAL DEFAULT 0.7")
        except sqlite3.OperationalError:
            pass

        try:
            conn.execute("ALTER TABLE subtitle_tasks ADD COLUMN bg_color TEXT DEFAULT '#000000'")
        except sqlite3.OperationalError:
            pass


init_db()


# ------------------ 工具函数 ------------------
def get_model(model_name):
    """获取模型，如果未加载则加载并缓存"""
    if model_name not in model_cache:
        logging.info(f"加载模型: {model_name}")
        model_cache[model_name] = whisper.load_model(model_name)
    return model_cache[model_name]


def format_timestamp(seconds):
    """将秒数格式化为 HH:MM:SS 时间戳"""
    m, s = divmod(seconds, 60)
    h, m = divmod(m, 60)
    return f"{int(h):02d}:{int(m):02d}:{int(s):02d}"

def format_srt_timestamp(seconds):
    """将秒数格式化为 SRT 标准时间戳 (HH:MM:SS,mmm)"""
    m, s = divmod(seconds, 60)
    h, m = divmod(m, 60)
    ms = int((seconds - int(seconds)) * 1000)
    return f"{int(h):02d}:{int(m):02d}:{int(s):02d},{ms:03d}"


def process_video(task_id, video_path, safe_name, model_name, language):
    """Asynchronously processes video transcription with DETAILED progress using FFmpeg for chunking."""
    with app.app_context():
        db = get_db()
        task_progress[task_id] = 0
        temp_chunk_files = []

        try:
            # --- Stage 1: Audio Extraction (Progress: 0% -> 20%) ---
            task_progress[task_id] = 1
            wav_path = OUT_DIR / f"{safe_name}.wav"

            # Use a direct ffmpeg call for extraction
            extract_command = ['ffmpeg', '-y', '-i', str(video_path), '-vn', '-acodec', 'pcm_s16le', '-ar', '16000',
                               '-ac', '1', str(wav_path)]
            subprocess.run(extract_command, check=True, capture_output=True)

            task_progress[task_id] = 20

            # --- Stage 2: Splitting Audio into Chunks using FFmpeg (Progress: 20% -> 30%) ---
            chunk_length_seconds = 30

            # Use ffprobe to get the audio duration to calculate the number of chunks
            try:
                probe_command = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of',
                                 'default=noprint_wrappers=1:nokey=1', str(wav_path)]
                duration_output = subprocess.check_output(probe_command).decode('utf-8').strip()
                total_duration = float(duration_output)
                total_chunks = math.ceil(total_duration / chunk_length_seconds)
            except Exception as e:
                logging.error(f"Could not get audio duration with ffprobe: {e}")
                # Fallback if ffprobe fails
                clip_temp = AudioFileClip(str(wav_path))
                total_duration = clip_temp.duration
                clip_temp.close()
                total_chunks = math.ceil(total_duration / chunk_length_seconds)

            # FFmpeg command to split the audio
            chunk_base_name = OUT_DIR / f"{safe_name}_chunk%03d.wav"  # e.g., chunk000.wav, chunk001.wav
            split_command = [
                'ffmpeg', '-i', str(wav_path), '-f', 'segment',
                '-segment_time', str(chunk_length_seconds),
                '-c', 'copy', str(chunk_base_name)
            ]
            subprocess.run(split_command, check=True, capture_output=True)

            # Populate the list of chunk files that were created
            for i in range(total_chunks):
                temp_chunk_files.append(OUT_DIR / f"{safe_name}_chunk{i:03d}.wav")

            task_progress[task_id] = 30

            # --- Stage 3: Transcribing Chunks (Progress: 30% -> 95%) ---
            model = get_model(model_name)
            lang_param = language if language != "auto" else None
            all_segments = []

            for i, chunk_path in enumerate(temp_chunk_files):
                if not chunk_path.exists(): continue  # Skip if a chunk wasn't created

                result = model.transcribe(str(chunk_path), language=lang_param)

                time_offset_seconds = i * chunk_length_seconds
                for seg in result['segments']:
                    seg['start'] += time_offset_seconds
                    seg['end'] += time_offset_seconds
                    all_segments.append(seg)

                progress = 30 + ((i + 1) / total_chunks) * 65
                task_progress[task_id] = min(progress, 95)

            # --- Stage 4: Combining and Saving SRT (Progress: 95% -> 100%) ---
            srt_content = ""
            srt_index = 1
            for seg in sorted(all_segments, key=lambda x: x['start']):  # Sort just in case
                start_time = format_srt_timestamp(seg["start"])
                end_time = format_srt_timestamp(seg["end"])
                text = seg['text'].strip()
                if text:
                    srt_content += f"{srt_index}\n{start_time} --> {end_time}\n{text}\n\n"
                    srt_index += 1

            srt_name = f"{safe_name}.srt"
            (OUT_DIR / srt_name).write_text(srt_content, encoding="utf-8")

            task_progress[task_id] = 100
            db.execute("UPDATE tasks SET status = 'completed', completed_at = ?, txt_name = ? WHERE id = ?",
                       (datetime.now(), srt_name, task_id))
            db.commit()

        except Exception as e:
            logging.error(f"Transcription task {task_id} failed: {e}")
            import traceback
            logging.error(traceback.format_exc())
            db.execute("UPDATE tasks SET status = 'failed' WHERE id = ?", (task_id,))
            db.commit()
        finally:
            task_progress.pop(task_id, None)
            if 'wav_path' in locals() and wav_path.exists():
                wav_path.unlink(missing_ok=True)
            for chunk_file in temp_chunk_files:
                if chunk_file.exists():
                    chunk_file.unlink(missing_ok=True)
            video_path.unlink(missing_ok=True)


def remove_filler_words(text):
    """去除文本中的语气词，保留时间戳格式"""
    # 将文本按行分割
    lines = text.split('\n')
    cleaned_lines = []

    for line in lines:
        # 检查行是否包含时间戳（格式如 [HH:MM:SS - HH:MM:SS]）
        if re.match(r'^\[\d{2}:\d{2}:\d{2} - \d{2}:\d{2}:\d{2}]', line):
            # 分割时间戳和文本内容
            parts = line.split(']', 1)
            timestamp = parts[0] + ']'
            content = parts[1].strip() if len(parts) > 1 else ""

            # 只对内容部分进行去杂处理
            if content:
                # 创建正则表达式模式，匹配所有语气词（前后可能有空格）
                pattern = r'\s*(' + '|'.join(re.escape(word) for word in FILLER_WORDS) + r')\s*'
                # 替换语气词为空格，然后去除多余空格
                cleaned_content = re.sub(pattern, ' ', content)
                cleaned_content = re.sub(r'\s+', ' ', cleaned_content).strip()

                # 重新组合时间戳和内容
                cleaned_line = f"{timestamp} {cleaned_content}"
            else:
                cleaned_line = timestamp
        else:
            # 没有时间戳的行直接保留
            cleaned_line = line

        cleaned_lines.append(cleaned_line)

    # 重新组合文本，确保每个时间戳行后面有一个空行
    return '\n'.join(cleaned_lines)


def parse_subtitles(subtitle_path):
    """解析字幕文件，返回 (字幕列表, 错误信息) 的元组"""
    subtitles = []
    logging.info(f"开始解析字幕文件: {subtitle_path}")

    # 尝试检测文件编码
    def detect_encoding(file_path):
        with open(file_path, 'rb') as f:
            raw_data = f.read(4096)
            result = chardet.detect(raw_data)
            return result['encoding'] or 'utf-8'

    encoding = detect_encoding(subtitle_path)
    logging.info(f"检测到字幕文件编码: {encoding}")

    # 尝试解析为SRT格式
    try:
        subs = pysrt.open(subtitle_path, encoding=encoding)
        for sub in subs:
            subtitles.append({
                'start': sub.start.ordinal / 1000.0,
                'end': sub.end.ordinal / 1000.0,
                'text': sub.text.replace('\n', ' ')
            })
        logging.info(f"成功解析为SRT格式: {len(subtitles)} 条字幕")
        return subtitles, None  # <-- CHANGE HERE
    except Exception as e:
        logging.warning(f"SRT解析失败: {e}")

    # 尝试解析为VTT格式
    try:
        subs = webvtt.read(subtitle_path, encoding=encoding)
        for sub in subs:
            subtitles.append({
                'start': sub.start_in_seconds,
                'end': sub.end_in_seconds,
                'text': sub.text.replace('\n', ' ')
            })
        logging.info(f"成功解析为VTT格式: {len(subtitles)} 条字幕")
        return subtitles, None  # <-- CHANGE HERE
    except Exception as e:
        logging.warning(f"VTT解析失败: {e}")

    # 尝试解析为ASS/SSA格式
    try:
        with open(subtitle_path, 'r', encoding=encoding) as f:
            lines = f.readlines()

        # ... (rest of ASS parsing logic is the same) ...
        in_events = False
        formats = []
        for line in lines:
            line = line.strip()
            if line == "[Events]":
                in_events = True
                continue
            if in_events and line.startswith("Format:"):
                formats = [f.strip().lower() for f in line.split(":", 1)[1].split(",")]
                continue
            if in_events and line.startswith("Dialogue:"):
                parts = line.split(",", len(formats) - 1)
                if len(parts) < len(formats):
                    continue
                try:
                    start_idx = formats.index("start")
                    end_idx = formats.index("end")
                    text_idx = formats.index("text")
                except ValueError:
                    start_idx = 1
                    end_idx = 2
                    text_idx = 9
                start_time = parts[start_idx].strip()
                end_time = parts[end_idx].strip()
                text = parts[text_idx].strip()
                text = re.sub(r"\{.*?}", "", text)

                def ass_time_to_seconds(time_str):
                    parts = time_str.split(":")
                    if len(parts) != 3: return 0
                    hours, minutes, seconds = parts
                    seconds_parts = seconds.split(".")
                    secs = int(seconds_parts[0])
                    millis = int(seconds_parts[1]) * 0.01 if len(seconds_parts) > 1 else 0.0
                    return int(hours) * 3600 + int(minutes) * 60 + secs + millis

                start_seconds = ass_time_to_seconds(start_time)
                end_seconds = ass_time_to_seconds(end_time)
                subtitles.append({'start': start_seconds, 'end': end_seconds, 'text': text})

        if subtitles:
            logging.info(f"成功解析为ASS/SSA格式: {len(subtitles)} 条字幕")
            return subtitles, None  # <-- CHANGE HERE
    except Exception as e:
        logging.warning(f"ASS/SSA解析失败: {e}")

    # 尝试解析为LRC格式（歌词格式）
    try:
        with open(subtitle_path, 'r', encoding=encoding) as f:
            for line in f:
                line = line.strip()
                if not line: continue
                matches = re.findall(r'\[(\d+):(\d+)(?:\.(\d+))?]', line)
                if matches:
                    text = re.sub(r'\[.*?]', '', line).strip()
                    if not text: continue
                    for match in matches:
                        minutes = int(match[0])
                        seconds = int(match[1])
                        milliseconds = int(match[2]) * 10 if match[2] else 0
                        start_time = minutes * 60 + seconds + milliseconds / 1000.0
                        end_time = start_time + 5.0
                        subtitles.append({'start': start_time, 'end': end_time, 'text': text})
        if subtitles:
            logging.info(f"成功解析为LRC格式: {len(subtitles)} 条字幕")
            return subtitles, None  # <-- CHANGE HERE
    except Exception as e:
        logging.warning(f"LRC解析失败: {e}")

    # 最终尝试：逐行解析作为纯文本
    try:
        with open(subtitle_path, 'r', encoding=encoding, errors='replace') as f:
            lines = [line.strip() for line in f.readlines() if line.strip()]
        has_timestamps = any(re.search(r'\d{1,2}:\d{2}:\d{2}', line) for line in lines)
        if not has_timestamps and lines:
            duration = 5
            for i, line in enumerate(lines):
                subtitles.append({'start': i * duration, 'end': (i + 1) * duration, 'text': line})
            logging.warning(f"使用纯文本模式解析: {len(subtitles)} 条字幕")
            return subtitles, None  # <-- CHANGE HERE
    except Exception as e:
        logging.error(f"纯文本解析失败: {e}")

    # 如果所有方法都失败，返回错误信息
    logging.error(f"无法解析字幕文件: {subtitle_path}")
    try:
        with open(subtitle_path, 'r', encoding=encoding, errors='replace') as f:
            preview = f.read(500)
        return [], f"无法解析字幕文件。文件内容预览:\n{preview}"
    except Exception as e:
        logging.error(f"无法预览文件内容: {e}")
        return [], f"无法解析字幕文件且无法读取内容: {str(e)}"
# --- HELPER FUNCTION TO CREATE A STYLED ASS FILE ---
def create_styled_ass(srt_path, ass_path, settings):
    """
    Converts an SRT file to a styled ASS file based on user settings.
    """

    # Helper to convert HEX to ASS format color (&HBBGGRR)
    def hex_to_ass_color(hex_color):
        if hex_color.startswith('#'):
            hex_color = hex_color[1:]
        r = hex_color[0:2]
        g = hex_color[2:4]
        b = hex_color[4:6]
        return f"&H00{b}{g}{r}".upper()

    # Get style settings
    font_size = settings.get('font_size', 24)
    font_color = hex_to_ass_color(settings.get('font_color', '#FFFFFF'))
    bg_color = hex_to_ass_color(settings.get('bg_color', '#000000'))

    # ASS format uses opacity from 00 (opaque) to FF (transparent)
    # We need to convert from 0.0-1.0 opacity to a hex value
    bg_opacity_hex = format(int((1 - settings.get('bg_opacity', 0.7)) * 255), '02X')

    # Position: 1=Bottom-Left, 2=Bottom-Center, 3=Bottom-Right, etc.
    position_map = {
        'bottom': 2,
        'middle': 5,
        'top': 8
    }
    alignment = position_map.get(settings.get('position', 'bottom'), 2)

    # Basic ASS header with style definition
    ass_header = f"""
[Script Info]
Title: Styled Subtitles
ScriptType: v4.00+
WrapStyle: 0
PlayResX: 1280
PlayResY: 720

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,SimHei,{font_size},{font_color},&H000000FF,&H00000000,&H{bg_opacity_hex}{bg_color[4:]},0,0,0,0,100,100,0,0,1,2,1,{alignment},10,10,10,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""

    # Read SRT and convert to ASS events
    subs = pysrt.open(srt_path, encoding='utf-8')
    ass_events = ""
    for sub in subs:
        start_time = f"{sub.start.hours:01}:{sub.start.minutes:02}:{sub.start.seconds:02}.{sub.start.milliseconds // 10:02}"
        end_time = f"{sub.end.hours:01}:{sub.end.minutes:02}:{sub.end.seconds:02}.{sub.end.milliseconds // 10:02}"
        # Remove newlines and add \N for line breaks in ASS
        text = sub.text.replace('\n', '\\N')
        ass_events += f"Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{text}\n"

    # Write the complete ASS file
    with open(ass_path, 'w', encoding='utf-8') as f:
        f.write(ass_header.strip() + "\n" + ass_events)


# --- REVISED, HIGH-PERFORMANCE FUNCTION ---
# In your input_file_0.py, find the add_subtitles_to_video_ffmpeg function
# and replace it with this version.

# In your input_file_0.py, replace the add_subtitles_to_video_ffmpeg function

#
# Replace the existing function in your project with this one.
#
def time_to_seconds(time_str):
    """Converts HH:MM:SS.ms time string to seconds."""
    try:
        h, m, s = map(float, time_str.split(':'))
        return h * 3600 + m * 60 + s
    except:
        return 0.0

def add_subtitles_to_video_ffmpeg(task_id, video_path, subtitle_path, settings):
    """Asynchronously adds subtitles to video using FFmpeg and reports progress."""
    with app.app_context():
        db = get_db()
        task_progress[task_id] = 0

        try:
            # Check for FFmpeg
            if not shutil.which("ffmpeg"):
                raise FileNotFoundError("FFmpeg not found!")

            # Get total duration of the video for progress calculation
            video_clip = VideoFileClip(str(video_path))
            total_duration = video_clip.duration
            video_clip.close()
            if total_duration == 0:
                raise ValueError("Video duration is zero, cannot calculate progress.")

            # Create styled ASS file
            styled_ass_path = subtitle_path.with_suffix('.ass')
            create_styled_ass(subtitle_path, styled_ass_path, settings)

            output_name = f"{uuid.uuid4().hex}.mp4"
            output_path = OUT_DIR / output_name

            # Path formatting for FFmpeg filter
            posix_path = styled_ass_path.as_posix()
            if ':' in posix_path:
                drive, path_part = posix_path.split(':', 1)
                escaped_path = f"{drive}\\:{path_part}"
            else:
                escaped_path = posix_path

            vf_argument = f"ass=filename='{escaped_path}'"

            command = [
                'ffmpeg',
                '-i', str(video_path),
                '-vf', vf_argument,
                '-c:v', 'libx264', '-c:a', 'aac', '-crf', '23', '-preset', 'fast',
                str(output_path),
                '-progress', 'pipe:1',  # This tells FFmpeg to output progress to stdout
                '-nostats'  # Quiets some other output
            ]

            # Use Popen to capture output in real-time
            process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                       universal_newlines=True, encoding='utf-8', errors='ignore')

            # Regex to find the time in FFmpeg's output
            time_re = re.compile(r"out_time_ms=(\d+)")

            for line in process.stdout:
                match = time_re.search(line)
                if match:
                    elapsed_ms = int(match.group(1))
                    elapsed_seconds = elapsed_ms / 1_000_000.0
                    progress = (elapsed_seconds / total_duration) * 100
                    task_progress[task_id] = min(progress, 100)  # Cap at 100

            process.wait()

            if process.returncode != 0:
                raise subprocess.CalledProcessError(process.returncode, command)

            # Task completed successfully
            task_progress[task_id] = 100
            db.execute("""UPDATE subtitle_tasks 
                          SET status = 'completed', completed_at = ?, output_name = ?
                          WHERE id = ?""",
                       (datetime.now(), output_name, task_id))
            db.commit()

        except Exception as e:
            logging.error(f"Subtitle adding task {task_id} failed: {str(e)}")
            db.execute("UPDATE subtitle_tasks SET status = 'failed' WHERE id = ?", (task_id,))
            db.commit()

        finally:
            # Clean up progress entry and files
            task_progress.pop(task_id, None)
            video_path.unlink(missing_ok=True)
            subtitle_path.unlink(missing_ok=True)
            if 'styled_ass_path' in locals() and styled_ass_path.exists():
                styled_ass_path.unlink()

# ------------------ 时间转换工具函数 ------------------
def parse_db_datetime(dt_str):
    """将数据库中的时间字符串转换为datetime对象"""
    if not dt_str:
        return None

    formats = [
        '%Y-%m-%d %H:%M:%S.%f',
        '%Y-%m-%d %H:%M:%S',
        '%Y-%m-%d %H:%M'
    ]

    for fmt in formats:
        try:
            return datetime.strptime(dt_str, fmt)
        except ValueError:
            continue

    return None


# (In tefs_mini.py)
# Replace the entire process_audio_replace function with this corrected version

# (In tefs_mini.py)
# Replace the entire process_audio_replace function with this definitive version

def process_audio_replace(task_id, video_path, audio_path, start_time, end_time):
    """Asynchronously processes audio replacement with direct FFmpeg progress reporting and silence padding."""
    with app.app_context():
        db = get_db()
        task_progress[task_id] = 0
        temp_files = []
        video = new_audio_clip = original_audio = final_audio = silence_clip = None

        try:
            video = VideoFileClip(str(video_path))
            video_duration = video.duration

            if start_time < 0: start_time = 0
            if end_time is None or end_time > video_duration: end_time = video_duration
            if start_time >= end_time: end_time = start_time + 1

            slot_duration = end_time - start_time

            new_audio_clip = AudioFileClip(str(audio_path))
            if new_audio_clip.duration > slot_duration:
                new_audio_clip = new_audio_clip.subclipped(0, slot_duration)

            if new_audio_clip.duration < slot_duration:
                silence_duration = slot_duration - new_audio_clip.duration
                silence_clip = AudioClip(lambda t: 0, duration=silence_duration, fps=new_audio_clip.fps)
                middle_audio_segment = concatenate_audioclips([new_audio_clip, silence_clip])
            else:
                middle_audio_segment = new_audio_clip

            original_audio = video.audio
            start_audio = original_audio.subclipped(0, start_time) if start_time > 0 else None
            end_audio = original_audio.subclipped(end_time, video_duration) if end_time < video_duration else None

            audio_clips_to_concat = [c for c in [start_audio, middle_audio_segment, end_audio] if c is not None]

            if audio_clips_to_concat:
                final_audio = concatenate_audioclips(audio_clips_to_concat)
                temp_audio_path = UPLOAD_DIR / f"temp_audio_{task_id}.aac"
                temp_files.append(temp_audio_path)
                final_audio.write_audiofile(str(temp_audio_path), codec='aac', logger=None)
            else:
                final_audio = None

            output_name = f"audio_replaced_{uuid.uuid4().hex}.mp4"
            output_path = OUT_DIR / output_name

            command = ['ffmpeg', '-y', '-i', str(video_path)]
            if final_audio:
                command.extend(['-i', str(temp_audio_path)])
                # --- START OF THE FIX ---
                # Replace '-c:v copy' with a full re-encode to enable progress reporting.
                command.extend([
                    '-c:v', 'libx264',  # Re-encode the video stream
                    '-preset', 'veryfast',  # Use a fast encoding preset for speed
                    '-crf', '23',  # Good balance of quality and file size
                    '-map', '0:v:0',  # Map video from the first input (original video)
                    '-map', '1:a:0',  # Map audio from the second input (our new audio)
                    '-c:a', 'aac',  # Ensure audio codec is AAC
                    '-shortest'  # Finish when the shortest stream ends
                ])
                # --- END OF THE FIX ---
            else:
                command.extend(['-c:v', 'copy', '-an'])  # This case is fine as it's instant

            command.extend([str(output_path), '-progress', 'pipe:1', '-nostats'])

            process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                       universal_newlines=True, encoding='utf-8', errors='ignore')

            time_re = re.compile(r"out_time_ms=(\d+)")
            for line in process.stdout:
                match = time_re.search(line)
                if match:
                    elapsed_ms = int(match.group(1))
                    elapsed_seconds = elapsed_ms / 1_000_000.0
                    if video_duration > 0:
                        progress = (elapsed_seconds / video_duration) * 100
                        task_progress[task_id] = min(progress, 100)

            process.wait()
            if process.returncode != 0:
                raise subprocess.CalledProcessError(process.returncode, command, output=process.stdout.read())

            task_progress[task_id] = 100
            db.execute(
                "UPDATE audio_replace_tasks SET status = 'completed', completed_at = ?, output_name = ? WHERE id = ?",
                (datetime.now(), output_name, task_id))
            db.commit()

        except Exception as e:
            logging.error(f"Audio replace task {task_id} failed: {e}")
            import traceback
            error_trace = traceback.format_exc()
            logging.error(error_trace)
            db.execute("UPDATE audio_replace_tasks SET status = 'failed' WHERE id = ?", (task_id,))
            db.commit()

        finally:
            for clip in [video, new_audio_clip, original_audio, final_audio, silence_clip]:
                if clip:
                    try:
                        clip.close()
                    except Exception:
                        pass  # Ignore errors on close
            task_progress.pop(task_id, None)
            video_path.unlink(missing_ok=True)
            audio_path.unlink(missing_ok=True)
            for temp_file in temp_files:
                temp_file.unlink(missing_ok=True)


# ------------------ 路由 ------------------
@app.route("/")
def index():
    return redirect(url_for("login"))


@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        u = request.form["username"].strip()
        p = request.form["password"]
        if not u or not p:
            flash("用户名和密码不能为空")
            return render_template("register.html")

        try:
            with get_db() as conn:
                hashed_pw = generate_password_hash(p)
                conn.execute("INSERT INTO users(username, password) VALUES (?,?)",
                             (u, hashed_pw))
            flash("注册成功，请登录")
            return redirect(url_for("login"))
        except sqlite3.IntegrityError:
            flash("用户名已存在")
    return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        u = request.form["username"]
        p = request.form["password"]
        user = get_db().execute("SELECT * FROM users WHERE username=?", (u,)).fetchone()

        if user and check_password_hash(user["password"], p):
            session["uid"] = user["id"]
            session["uname"] = user["username"]
            return redirect(url_for("features"))  # 修改为跳转到功能选择页面
        flash("用户名或密码错误")
    return render_template("login.html")


@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))


# 新增功能选择页面路由
@app.route("/features")
def features():
    if "uid" not in session:
        return redirect(url_for("login"))
    return render_template("features.html", username=session["uname"])


@app.route("/dashboard")
def dashboard():
    if "uid" not in session:
        return redirect(url_for("login"))

    # 获取用户的所有任务（按创建时间倒序）
    tasks = get_db().execute("""
        SELECT id, original_name, model_name, language, status, created_at, completed_at, txt_name, cleaned
        FROM tasks 
        WHERE user_id = ?
        ORDER BY created_at DESC
    """, (session["uid"],)).fetchall()

    # 处理日期格式 - 转换为datetime对象
    formatted_tasks = []
    for task in tasks:
        task_dict = dict(task)
        # 转换创建时间
        task_dict['created_at'] = parse_db_datetime(task_dict['created_at'])
        # 转换完成时间
        task_dict['completed_at'] = parse_db_datetime(task_dict['completed_at'])
        formatted_tasks.append(task_dict)

    return render_template("dashboard.html",
                           username=session["uname"],
                           tasks=formatted_tasks,
                           models=SUPPORTED_MODELS,
                           languages=SUPPORTED_LANGUAGES)


# (In tefs_mini.py)
# Replace the old /upload route with this one

@app.route("/upload", methods=["POST"])
def upload():
    if "uid" not in session:
        return jsonify({"success": False, "message": "未登录"}), 401

    file = request.files.get("video")
    if not file or file.filename == "":
        return jsonify({"success": False, "message": "未选择文件"}), 400

    ext = file.filename.rsplit(".", 1)[-1].lower()
    if ext not in {"mp4", "mov", "mkv", "avi", "webm"}:
        return jsonify({"success": False, "message": "视频格式不支持"}), 400

    model_name = request.form.get("model", "small")
    language = request.form.get("language", "auto")

    if model_name not in SUPPORTED_MODELS: model_name = "small"
    if language not in SUPPORTED_LANGUAGES: language = "auto"

    safe_name = f"{uuid.uuid4().hex}.{ext}"
    video_path = UPLOAD_DIR / safe_name
    file.save(video_path)

    with get_db() as conn:
        cur = conn.execute(
            "INSERT INTO tasks (user_id, original_name, safe_name, model_name, language, status) VALUES (?, ?, ?, ?, ?, 'processing')",
            (session["uid"], file.filename, safe_name, model_name, language)
        )
        task_id = cur.lastrowid
        conn.commit()

    executor.submit(process_video, task_id, video_path, safe_name, model_name, language)

    # Return the task_id so the frontend knows which task to track
    return jsonify({"success": True, "message": "任务已开始", "task_id": task_id})


@app.route("/transcription_progress/<int:task_id>")
def transcription_progress(task_id):
    if "uid" not in session:
        return jsonify({"error": "Unauthorized"}), 401

    # Check if the user is authorized to view this task's progress (optional but good practice)
    task = get_db().execute("SELECT 1 FROM tasks WHERE id = ? AND user_id = ?", (task_id, session["uid"])).fetchone()
    if not task:
        return jsonify({"error": "Task not found or unauthorized"}), 404

    progress = task_progress.get(task_id, 0)
    return jsonify({"progress": progress})

@app.route("/download/<filename>")
def download(filename):
    if "uid" not in session:
        return redirect(url_for("login"))

    # 验证文件属于当前用户
    task = get_db().execute("""
        SELECT 1 FROM tasks 
        WHERE user_id = ? AND txt_name = ?
    """, (session["uid"], filename)).fetchone()

    if not task:
        flash("文件不存在或无权访问")
        return redirect(url_for("dashboard"))

    return send_from_directory(OUT_DIR, filename, as_attachment=True)


# (In tefs_mini.py)
# Add this new route anywhere with your other routes

@app.route("/audio_replace_progress/<int:task_id>")
def audio_replace_progress(task_id):
    if "uid" not in session:
        return jsonify({"error": "Unauthorized"}), 401

    task = get_db().execute("SELECT 1 FROM audio_replace_tasks WHERE id = ? AND user_id = ?",
                            (task_id, session["uid"])).fetchone()
    if not task:
        return jsonify({"error": "Task not found or unauthorized"}), 404

    progress = task_progress.get(task_id, 0)
    return jsonify({"progress": progress})

@app.route("/clean/<int:task_id>")
def clean_task(task_id):
    if "uid" not in session:
        return redirect(url_for("login"))

    # 验证任务属于当前用户
    task = get_db().execute("""
        SELECT txt_name, cleaned FROM tasks 
        WHERE id = ? AND user_id = ? AND status = 'completed'
    """, (task_id, session["uid"])).fetchone()

    if not task:
        flash("任务不存在或已完成")
        return redirect(url_for("dashboard"))

    # 如果已经清理过，直接返回
    if task["cleaned"]:
        flash("该任务已清理过")
        return redirect(url_for("dashboard"))

    # 读取原始文件
    txt_path = OUT_DIR / task["txt_name"]
    if not txt_path.exists():
        flash("文本文件不存在")
        return redirect(url_for("dashboard"))

    # 处理文件
    try:
        original_text = txt_path.read_text(encoding="utf-8")
        cleaned_text = remove_filler_words(original_text)

        # 创建新的文件名（添加_cleaned后缀）
        base_name = task["txt_name"].rsplit(".", 1)[0]
        new_filename = f"{base_name}_cleaned.txt"
        cleaned_path = OUT_DIR / new_filename
        cleaned_path.write_text(cleaned_text, encoding="utf-8")

        # 更新数据库
        with get_db() as conn:
            conn.execute("""
                UPDATE tasks 
                SET txt_name = ?, cleaned = 1 
                WHERE id = ?
            """, (new_filename, task_id))
            conn.commit()

        flash("去杂处理完成")
    except Exception as e:
        logging.error(f"去杂处理失败: {e}")
        flash("去杂处理失败")

    return redirect(url_for("dashboard"))


@app.route("/profile", methods=["GET", "POST"])
def profile():
    if "uid" not in session:
        return redirect(url_for("login"))

    user_id = session["uid"]
    db = get_db()

    if request.method == "POST":
        username = request.form["username"].strip()
        old_password = request.form["old_password"]
        new_password = request.form["new_password"]
        confirm_password = request.form["confirm_password"]

        # 验证用户信息
        user = db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
        if not user:
            flash("用户不存在")
            return redirect(url_for("dashboard"))

        # 验证旧密码
        if not check_password_hash(user["password"], old_password):
            flash("旧密码错误")
            return render_template("profile.html", username=user["username"])

        # 更新用户名
        if username != user["username"]:
            try:
                db.execute("UPDATE users SET username = ? WHERE id = ?", (username, user_id))
                db.commit()
                session["uname"] = username
            except sqlite3.IntegrityError:
                flash("用户名已被使用")
                return render_template("profile.html", username=user["username"])

        # 更新密码
        if new_password:
            if new_password != confirm_password:
                flash("新密码和确认密码不匹配")
                return render_template("profile.html", username=username)

            hashed_pw = generate_password_hash(new_password)
            db.execute("UPDATE users SET password = ? WHERE id = ?", (hashed_pw, user_id))
            db.commit()
            flash("密码更新成功")

        flash("用户信息更新成功")
        return redirect(url_for("dashboard"))

    # GET请求显示用户信息
    user = db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
    return render_template("profile.html", username=user["username"])


@app.route("/check_tasks")
def check_tasks():
    if "uid" not in session:
        return jsonify({"error": "未登录"}), 401

    # 获取用户正在处理的任务
    tasks = get_db().execute("""
        SELECT id, status
        FROM tasks 
        WHERE user_id = ? AND status = 'processing'
    """, (session["uid"],)).fetchall()

    # 如果有处理中的任务返回 True，否则返回 False
    return jsonify({"processing": len(tasks) > 0})


@app.route("/add_subtitles", methods=["GET", "POST"])
def add_subtitles():
    if "uid" not in session:
        return redirect(url_for("login"))

    if request.method == "GET":
        tasks = get_db().execute("""
            SELECT id, video_name, position, status, created_at, completed_at
            FROM subtitle_tasks WHERE user_id = ? ORDER BY created_at DESC LIMIT 10
        """, (session["uid"],)).fetchall()
        formatted_tasks = []
        for task in tasks:
            task_dict = dict(task)
            task_dict['created_at'] = parse_db_datetime(task_dict['created_at'])
            task_dict['completed_at'] = parse_db_datetime(task_dict['completed_at'])
            formatted_tasks.append(task_dict)
        return render_template("add_subtitles.html",
                               username=session["uname"],
                               subtitle_tasks=formatted_tasks,
                               allowed_ext=ALLOWED_SUBTITLE_EXT)

    else:  # POST Request
        # ... (keep all the file validation logic from your original function)
        video_file = request.files.get('video')
        subtitle_file = request.files.get('subtitle')

        if not video_file or video_file.filename == '':
            return jsonify({"success": False, "message": "未选择视频文件"}), 400
        if not subtitle_file or subtitle_file.filename == '':
            return jsonify({"success": False, "message": "未选择字幕文件"}), 400

        video_ext = video_file.filename.rsplit(".", 1)[-1].lower()
        if video_ext not in {"mp4", "mov", "mkv", "avi", "webm"}:
            return jsonify({"success": False, "message": "视频格式不支持"}), 400

        subtitle_ext = subtitle_file.filename.rsplit(".", 1)[-1].lower()
        if subtitle_ext not in ALLOWED_SUBTITLE_EXT:
            return jsonify({"success": False, "message": f"字幕格式不支持"}), 400

        # Save files
        video_safe_name = f"{uuid.uuid4().hex}.{video_ext}"
        video_path = UPLOAD_DIR / video_safe_name
        video_file.save(video_path)

        subtitle_safe_name = f"{uuid.uuid4().hex}.{subtitle_ext}"
        subtitle_path = UPLOAD_DIR / subtitle_safe_name
        subtitle_file.save(subtitle_path)

        # Get settings
        try:
            font_size = int(request.form.get('font_size', 24))
            font_color = request.form.get('font_color', '#FFFFFF')
            bg_color = request.form.get('bg_color', '#000000')
            bg_opacity = float(request.form.get('bg_opacity', 0.7))
            position = request.form.get('position', 'bottom')
        except Exception as e:
            return jsonify({"success": False, "message": f"参数错误: {str(e)}"}), 400

        # Create task in database
        with get_db() as conn:
            cur = conn.execute("""
                INSERT INTO subtitle_tasks (
                    user_id, video_name, video_safe_name, subtitle_name, 
                    font_size, font_color, bg_color, bg_opacity, position, status
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'processing')
            """, (session["uid"], video_file.filename, video_safe_name, subtitle_safe_name,
                  font_size, font_color, bg_color, bg_opacity, position))
            task_id = cur.lastrowid
            conn.commit()

        settings = {'font_size': font_size, 'font_color': font_color, 'bg_color': bg_color, 'bg_opacity': bg_opacity,
                    'position': position}
        executor.submit(add_subtitles_to_video_ffmpeg, task_id, video_path, subtitle_path, settings)

        # IMPORTANT: Return the task_id to the client
        return jsonify({"success": True, "message": "任务已开始处理", "task_id": task_id})


# --- ADD THIS NEW ROUTE somewhere with your other routes ---
@app.route("/task_progress/<int:task_id>")
def task_progress_route(task_id):
    if "uid" not in session:
        return jsonify({"error": "未登录"}), 401

    # You might want to add another check here to ensure the user owns the task

    progress = task_progress.get(task_id, 0)
    return jsonify({"progress": progress})

@app.route("/download_subtitle/<int:task_id>")
def download_subtitle(task_id):
    if "uid" not in session:
        return redirect(url_for("login"))

    # 验证任务属于当前用户
    task = get_db().execute("""
        SELECT output_name FROM subtitle_tasks 
        WHERE id = ? AND user_id = ? AND status = 'completed'
    """, (task_id, session["uid"])).fetchone()

    if not task or not task["output_name"]:
        flash("文件不存在或无权访问")
        return redirect(url_for("add_subtitles"))

    return send_from_directory(OUT_DIR, task["output_name"], as_attachment=True)


@app.route("/preview_subtitle/<int:task_id>")
def preview_subtitle(task_id):
    if "uid" not in session:
        return jsonify({"error": "未登录"}), 401

    # 验证任务属于当前用户
    task = get_db().execute("""
        SELECT subtitle_name FROM subtitle_tasks 
        WHERE id = ? AND user_id = ?
    """, (task_id, session["uid"])).fetchone()

    if not task:
        return jsonify({"error": "任务不存在或无权访问"}), 404

    subtitle_path = UPLOAD_DIR / task["subtitle_name"]
    if not subtitle_path.exists():
        return jsonify({"error": "字幕文件不存在"}), 404

    try:
        # 尝试检测编码
        with open(subtitle_path, 'rb') as f:
            raw_data = f.read(4096)
            result = chardet.detect(raw_data)
            encoding = result['encoding'] or 'utf-8'

        # 读取前1000个字符
        with open(subtitle_path, 'r', encoding=encoding, errors='replace') as f:
            content = f.read(1000)

        return jsonify({"content": content})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/audio_replace", methods=["GET", "POST"])
def audio_replace():
    if "uid" not in session:
        return redirect(url_for("login"))

    if request.method == "GET":
        # 获取用户的所有音频替换任务（按创建时间倒序）
        tasks = get_db().execute("""
            SELECT id, video_name, audio_name, start_time, end_time, status, created_at, completed_at
            FROM audio_replace_tasks 
            WHERE user_id = ?
            ORDER BY created_at DESC
            LIMIT 10
        """, (session["uid"],)).fetchall()

        # 处理日期格式
        formatted_tasks = []
        for task in tasks:
            task_dict = dict(task)
            task_dict['created_at'] = parse_db_datetime(task_dict['created_at'])
            task_dict['completed_at'] = parse_db_datetime(task_dict['completed_at'])
            formatted_tasks.append(task_dict)

        return render_template("audio_replace.html",
                               username=session["uname"],
                               tasks=formatted_tasks,
                               audio_ext=SUPPORTED_AUDIO_EXT)

    else:  # POST 请求
        # 获取上传的文件
        video_file = request.files.get('video')
        audio_file = request.files.get('audio')

        if not video_file or video_file.filename == '':
            return jsonify({"success": False, "message": "未选择视频文件"}), 400

        if not audio_file or audio_file.filename == '':
            return jsonify({"success": False, "message": "未选择音频文件"}), 400

        # 检查文件扩展名
        video_ext = video_file.filename.rsplit(".", 1)[-1].lower()
        if video_ext not in {"mp4", "mov", "mkv", "avi", "webm"}:
            return jsonify({"success": False, "message": "视频格式不支持"}), 400

        audio_ext = audio_file.filename.rsplit(".", 1)[-1].lower()
        if audio_ext not in SUPPORTED_AUDIO_EXT:
            return jsonify({
                "success": False,
                "message": f"音频格式不支持（仅支持: {', '.join(SUPPORTED_AUDIO_EXT)}）"
            }), 400

        # 获取时间参数
        try:
            start_time = float(request.form.get('start_time', 0))
            end_time_str = request.form.get('end_time', '')
            end_time = float(end_time_str) if end_time_str else None
        except ValueError:
            return jsonify({"success": False, "message": "时间参数格式错误"}), 400

        # 保存视频文件
        video_safe_name = f"{uuid.uuid4().hex}.{video_ext}"
        video_path = UPLOAD_DIR / video_safe_name
        video_file.save(video_path)

        # 保存音频文件
        audio_safe_name = f"{uuid.uuid4().hex}.{audio_ext}"
        audio_path = UPLOAD_DIR / audio_safe_name
        audio_file.save(audio_path)

        # 创建任务记录
        with get_db() as conn:
            cur = conn.execute("""
                INSERT INTO audio_replace_tasks (
                    user_id, video_name, video_safe_name, 
                    audio_name, audio_safe_name,
                    start_time, end_time, status
                ) VALUES (?, ?, ?, ?, ?, ?, ?, 'processing')
            """, (
                session["uid"],
                video_file.filename,
                video_safe_name,
                audio_file.filename,
                audio_safe_name,
                start_time,
                end_time
            ))
            task_id = cur.lastrowid
            conn.commit()

        # 异步处理任务
        executor.submit(process_audio_replace, task_id, video_path, audio_path, start_time, end_time)

        return jsonify({"success": True, "message": "音频替换任务已开始处理", "task_id": task_id})


@app.route("/download_audio_replace/<int:task_id>")
def download_audio_replace(task_id):
    if "uid" not in session:
        return redirect(url_for("login"))

    # 验证任务属于当前用户
    task = get_db().execute("""
        SELECT output_name FROM audio_replace_tasks 
        WHERE id = ? AND user_id = ? AND status = 'completed'
    """, (task_id, session["uid"])).fetchone()

    if not task or not task["output_name"]:
        flash("文件不存在或无权访问")
        return redirect(url_for("audio_replace"))

    return send_from_directory(OUT_DIR, task["output_name"], as_attachment=True)


# ------------------ 启动 ------------------
if __name__ == "__main__":
    # 预加载默认模型
    get_model("small")
    app.run(debug=True)